package swpy.fickle {
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import mx.utils.Base64Decoder;

	public class Unfickler {
		private var buffer:ByteArray;
		private var stack:Array;
		private var memo:Array;
		private var dispatch:*;
		private var marker:Object;

		public function Unfickler() {
			stack = new Array();
			memo = new Array();
			marker = new Object();
			dispatch = new Object();
			
			dispatch[Opcodes.PROTO] = loadProto;
			dispatch[Opcodes.NONE] = loadNone;
			dispatch[Opcodes.NEWTRUE] = loadTrue;
			dispatch[Opcodes.NEWFALSE] = loadFalse;
			dispatch[Opcodes.BINFLOAT] = loadBinFloat;
			dispatch[Opcodes.BININT] = loadBinInt;
			dispatch[Opcodes.BININT1] = loadBinInt1;
			dispatch[Opcodes.BININT2] = loadBinInt2;
			dispatch[Opcodes.SHORT_BINBYTES] = loadShortBinBytes;
			dispatch[Opcodes.BINBYTES] = loadBinBytes;
			dispatch[Opcodes.BINUNICODE] = loadBinUnicode;
			dispatch[Opcodes.SHORT_BINSTRING] = loadShortBinString;
			dispatch[Opcodes.BINSTRING] = loadBinUnicode; // Same as BINUNICODE
			dispatch[Opcodes.TUPLE] = loadTuple;
			dispatch[Opcodes.EMPTY_TUPLE] = loadEmptyTuple;
			dispatch[Opcodes.TUPLE1] = loadTuple1;
			dispatch[Opcodes.TUPLE2] = loadTuple2;
			dispatch[Opcodes.TUPLE3] = loadTuple3;
			dispatch[Opcodes.EMPTY_DICT] = loadEmptyDictionary;
			dispatch[Opcodes.EMPTY_LIST] = loadEmptyList;
			dispatch[Opcodes.DICT] = loadDict;
			dispatch[Opcodes.LIST] = loadTuple; // Same as TUPLE
			dispatch[Opcodes.APPEND] = loadAppend;
			dispatch[Opcodes.APPENDS] = loadAppends;
			dispatch[Opcodes.SETITEM] = loadSetItem;
			dispatch[Opcodes.SETITEMS] = loadSetItems;
			dispatch[Opcodes.POP] = loadPop;
			dispatch[Opcodes.POP_MARK] = loadPopMark;
			dispatch[Opcodes.DUP] = loadDup;
			dispatch[Opcodes.MARK] = loadMark;
			dispatch[Opcodes.STOP] = loadStop;
			dispatch[Opcodes.BINGET] = loadBinGet;
			dispatch[Opcodes.LONG_BINGET] = loadLongBinGet;
			dispatch[Opcodes.BINPUT] = loadBinPut;
			dispatch[Opcodes.LONG_BINPUT] = loadLongBinPut;
			dispatch[Opcodes.EXT1] = loadExt1;
			dispatch[Opcodes.EXT2] = loadExt2;
			dispatch[Opcodes.EXT4] = loadExt4;
		}
		
		public function load(fickled:ByteArray):Object {
			buffer = fickled;
			buffer.position = 0;
			try {
				while (true) {
					var key:uint = uint(buffer.readByte()) & 0xFF;
					//trace(key.toString(16));
					var func:Function = dispatch[key];
					if (func != null)
						func();
					else
						throw "Unknown opcode: " + key.toString(16);
				}
			}
			catch (stopInst: Stop) {
				return stopInst.value;
			}
			return null; 
		}
		
		private function loadProto():void {
			var proto:uint = buffer.readByte();
			if (proto != 3)
				throw "Protocol version must be 0x03"
		}

		private function loadNone():void {
			stack.push(null);
		}
		
		private function loadTrue():void {
			stack.push(true);			
		}
		
		private function loadFalse():void {
			stack.push(false);			
		}
		
		private function loadBinInt():void {
			buffer.endian = Endian.LITTLE_ENDIAN;
		    stack.push(buffer.readInt());
		}

		private function loadBinInt1():void {
		    stack.push(uint(buffer.readByte()) & 0xFF);
		}
		
		private function loadBinInt2():void {
			buffer.endian = Endian.LITTLE_ENDIAN;
		    stack.push(uint(buffer.readShort()) & 0xFFFF);
		}

		private function loadBinFloat():void {
			buffer.endian = Endian.BIG_ENDIAN;
			stack.push(buffer.readDouble());
		}

		private function loadShortBinString():void {
			var len:uint = uint(buffer.readByte()) & 0xFF;
		    stack.push(buffer.readUTFBytes(len));	
		}
		
		private function loadBinUnicode():void {
			buffer.endian = Endian.LITTLE_ENDIAN;
			var len:uint = buffer.readInt();
		    stack.push(buffer.readUTFBytes(len));	
		}

		private function loadBinBytes():void {
			buffer.endian = Endian.LITTLE_ENDIAN;
			var len:uint = buffer.readInt();
			var bytes:ByteArray = new ByteArray();
			buffer.readBytes(bytes, 0, len)
			bytes.position = 0;
		    stack.push(bytes);	
		}

		private function loadShortBinBytes():void {
			var len:uint = uint(buffer.readByte()) & 0xFF;
			buffer.endian = Endian.LITTLE_ENDIAN;
			var bytes:ByteArray = new ByteArray();
			buffer.readBytes(bytes, 0, len)
			bytes.position = 0;
		    stack.push(bytes);	
		}
		
		private function loadEmptyTuple():void {
			stack.push(new Array());
		}

		private function loadTuple():void {
			var mark:uint = stack.lastIndexOf(marker);
			stack[mark] = stack.slice(mark + 1);
		}
		
		private function loadTuple1():void {
			stack[stack.length - 1] = new Array(stack[stack.length - 1]);
		}
		
		private function loadTuple2():void {
			stack[stack.length - 1] = new Array(stack[stack.length - 2],
				stack[stack.length - 1])
		}
		
		private function loadTuple3():void {
			stack[stack.length - 1] = new Array(stack[stack.length - 3],
				stack[stack.length - 2], stack[stack.length - 1]);
		}
		
		private function loadEmptyList():void {
			stack.push(new Array());
		}
		
		private function loadEmptyDictionary():void {
			stack.push(new Object());
		}
		
		private function loadDict():void {
			var mark:uint = stack.lastIndexOf(marker);
			var object:*;
			var items:Array = stack.slice(mark + 1);
			for (var i:uint = 0; i < items.length - 1; i += 2) {
				var key:* = items[i];
				var value:* = items[i + 1];
				object[key] = value;
			}
			stack[mark] = object;	
		}
		
		private function loadAppend():void {
			var value:* = stack.pop();
			var list:* = stack[stack.length - 1];
			list.push(value);
		}
		
		private function loadAppends():void {
			var mark:uint = stack.lastIndexOf(marker);
			var list:* = stack[mark - 1];
			stack[mark - 1] = list.concat(stack.slice(mark + 1))
			stack.splice(mark);
		}

		private function loadSetItem():void {
			var value:* = stack.pop();
			var key:* = stack.pop();
			stack[stack.length - 1][key] = value;
		}

		private function loadSetItems():void {
			var mark:uint = stack.lastIndexOf(marker);
			var dict:* = stack[mark - 1];
			for (var i:uint = mark + 1; i <= stack.length; i += 2) {
				dict[stack[i]] = stack[i + 1];
			}
			stack.splice(mark);
		}
		
		private function loadPop():void {
			stack.pop();
		}
		
		private function loadPopMark():void {
			var mark:uint = stack.lastIndexOf(marker);
			stack.splice(mark);
		}
		
		private function loadDup():void {
			stack.push(stack[stack.length - 1]);
		}
		
		private function loadBinGet():void {
			var idx:uint = uint(buffer.readByte()) & 0xFF;
			stack.push(memo[idx]);
		}

		private function loadLongBinGet():void {
			buffer.endian = Endian.LITTLE_ENDIAN;
			var idx:uint = uint(buffer.readInt()) & 0xFFFFFFFF;
			stack.push(memo[idx]);
		}

		private function loadBinPut():void {
			buffer.position++;
			memo.push(stack[stack.length - 1]);
		}

		private function loadLongBinPut():void {
			buffer.position += 4;
			memo.push(stack[stack.length - 1]);
		}

		private function loadExt1():void {
			buffer.position++;
		}

		private function loadExt2():void {
			buffer.position += 2;
		}

		private function loadExt4():void {
			buffer.position += 4;
		}

		private function loadMark():void {
			stack.push(marker);
		}
		
		private function loadStop():void {
			throw new Stop(stack.pop());
		}

		public static function loads(fickled:String) : Object {
			var decoder:Base64Decoder = new Base64Decoder();
			decoder.decode(fickled);
			var ba:ByteArray = decoder.toByteArray();
			ba.uncompress();
			return (new Unfickler()).load(ba);
		}
	}
}

private class Stop extends Error {
	public var value:Object;

	public function Stop(val:Object) {
		value = val;
	}
}